home *** CD-ROM | disk | FTP | other *** search
- /* QBE.m:
- * You may freely copy, distribute, and reuse the code in this example.
- * NeXT disclaims any warranty of any kind, expressed or implied, as to its
- * fitness for any particular use.
- *
- * Written by Craig Federighi
- *
- *
- * Example of a Query By Example object: Connects to an EOController for an
- * easy contruction of qualifiers from data entered into fields of an
- * existing user interface.
- */
-
- #import "QBE.h"
- #import "DictionaryDataSource.h"
-
- #import <eoaccess/eoaccess.h>
- #import <foundation/NSString.h>
- #import <foundation/NSArray.h>
-
- #import <foundation/NSCharacterSet.h>
- #import <foundation/NSScanner.h>
-
-
-
- /* A private category to strip white spaces */
-
- @interface NSString(_QBEStripWhite)
- - (NSString *)stringStrippingLeadingCharacterSet:(NSCharacterSet *)skipset;
- @end
-
- @implementation NSString(_QBEStripWhite)
-
- /* Returns autoreleased string produced by generating new string
- * with the leading junk skipped.
- */
- - (NSString *)stringStrippingLeadingCharacterSet:(NSCharacterSet *)skipset
- {
- NSRange range;
-
- range = [self rangeOfCharacterFromSet: [skipset invertedSet]];
- return [self substringFromIndex: range.location];
- }
-
- @end
-
- /* A category on EOAssociation to turn on and off editability of
- * underlying UI objects
- */
- @interface EOAssociation(_QBEEditable)
- - (BOOL)isEditable;
- - (void)setEditable:(BOOL)yn;
- @end
-
- @implementation EOAssociation(_QBEEditable)
- - (BOOL)isEditable { return NO; }
- - (void)setEditable:(BOOL)yn {}
- @end
-
- @implementation EOControlAssociation(_QBEEditable)
- - (BOOL)isEditable {
- Cell * cell;
- if ([[self destination] isKindOfClass:[ActionCell class]])
- cell = [self destination];
- else
- cell = [[self destination] cell];
-
- return [cell isEditable];
- }
-
- - (void)setEditable:(BOOL)yn {
- Cell * cell;
- if ([[self destination] isKindOfClass:[ActionCell class]])
- cell = [self destination];
- else
- cell = [[self destination] cell];
-
- [cell setEditable:yn];
- }
- @end
-
- @implementation EOColumnAssociation(_QBEEditable)
- - (BOOL)isEditable {return [tableView isEditable]; }
- - (void)setEditable:(BOOL)yn
- {
- [tableView setEditable:yn];
- }
- @end
-
-
- @implementation EOActionCellAssociation(_QBEEditable)
- - (BOOL)isEditable {
- return [(Cell *)[self destination] isEditable];
- }
-
- - (void)setEditable:(BOOL)yn {
- [(Cell *)[self destination] setEditable:yn];
- }
- @end
-
-
- /* Need to tell what sort of associations can participate in QBE
- * editing. Master-detail associations, for example, should be
- * deactivated.
- */
- @interface EOAssociation(_isQBEEditor)
- - (BOOL)isQBEEditor; // Should remain active in QBE editing
- @end
-
- @implementation EOAssociation(_isQBEEditor)
- - (BOOL)isQBEEditor {return YES;}
- @end
-
- @implementation EOQualifiedAssociation(_isQBEEditor)
- - (BOOL)isQBEEditor {return NO;}
- @end
-
- // Make sure an Adaptor is ready to convert values
- // (the SybaseAdaptor can't do this until after it's connected)
- @interface EOAdaptorChannel (canConvert)
- - (BOOL)canConvertValues;
- @end
-
- @implementation EOAdaptorChannel (canConvert)
- - (BOOL)canConvertValues
- {
- EOAdaptorChannel *adaptorChannel = nil;
- EOAdaptor *adaptor;
- BOOL retValue = YES;
-
- if (![self isOpen]) {
- adaptor = [[adaptorChannel adaptorContext] adaptor];
- if (![adaptor hasValidConnectionDictionary]){
- if (![adaptor runLoginPanelAndValidateConnectionDictionary])
- retValue = NO;
- }
-
- if (retValue == YES && ![self openChannel])
- retValue = NO;
- }
- return retValue;
- }
- @end
-
-
-
- /* Actual implementation of QBE object */
-
- @implementation QBE
-
- - init
- {
- return self;
- }
-
- static NSString *operators[] = {
- @"=",
- @">",
- @"<",
- @"<=",
- @">=",
- NULL
- };
-
-
- /* Build the qualifier based on the user input */
-
- - qualifierForKey:(NSString *)key value:value entity:(EOEntity *)entity
- {
- EOAdaptorChannel *adaptorChannel = [[realsource databaseChannel] adaptorChannel];
- EOAdaptor *adaptor = [[adaptorChannel adaptorContext] adaptor];
- NSString **ops;
- NSString *op = nil, *fmt;
- id target = value;
- EOQualifier *qualifier;
- EOAttribute *attr;
- BOOL isStringAttribute;
-
- // Be sure that we can connect to the database before constructing a
- // qualifier. Without a connection, the adaptor can't
- // formatValue:forAttribute:
- if (![adaptorChannel canConvertValues])
- return nil;
-
- [adaptorChannel setDebugEnabled:YES];
-
- // Check type of attribute so we know whether to quote target
- attr = [entity attributeNamed:key];
- isStringAttribute = (strcmp([attr valueClassName], "NSString") == 0);
-
- // Look for an operator. If we see one, rip it off and use it
- if ([value isKindOfClass:[NSString class]]) {
- for(ops=operators; *ops; ops++) {
- if ( [(NSString *)value hasPrefix: *ops] ) {
- op = *ops;
-
- // Need to strip whitespace!
- target = [[(NSString *)value substringFromIndex: [op length]]
- stringStrippingLeadingCharacterSet:
- [NSCharacterSet whitespaceCharacterSet]];
- break;
- }
- }
- }
-
- // compute format string
- if (isStringAttribute && !op) {
- // Wildcard search
- op = @"LIKE";
- target = [NSString stringWithFormat:@"%%%@%%", target];
- } else if (!op) {
- // Default for non string attributes
- op = @"=";
- }
-
- fmt = @"%A %@ %@";
- qualifier = [[EOQualifier alloc] initWithEntity:entity
- qualifierFormat:fmt, key, op,
- [adaptor formatValue:target forAttribute:attr]];
- return [qualifier autorelease];
- }
-
-
- - (EOQualifier *)makeQualifierForEo:eo entity:(EOEntity *)entity
- conjoin:(BOOL)isConjoin
- // Construct a qualifier for an EO
- {
- NSDictionary *valueDict;
- NSArray *attributes;
- NSMutableArray *attrnames;
- int count, i;
- EOQualifier *qualifier, *root = nil;
-
- // get a dictionary of values for the eo
- attributes = [entity attributes];
-
- count = [attributes count];
- attrnames = [NSMutableArray arrayWithCapacity:count];
-
- // We should really only ask for the classAttributes
- for(i=0; i<count; i++)
- [attrnames addObject:
- [(EOAttribute *)[attributes objectAtIndex:i] name]];
-
- valueDict = [eo valuesForKeys:attrnames];
-
- for(i=0; i<count; i++) {
- NSString *key = [attrnames objectAtIndex:i];
- id value = [valueDict objectForKey:key];
- if( value ) {
- qualifier = [self qualifierForKey:key value:value entity:entity];
- if (qualifier) {
- if (root) {
- if (isConjoin)
- [root conjoinWithQualifier:qualifier];
- else
- [root disjoinWithQualifier:qualifier];
- } else
- root = qualifier;
- }
- }
- }
-
- return root;
- }
-
- - makeQualifier:(BOOL)isConjoin
- {
- EOEntity *entity;
- NSArray *objects;
- EOQualifier *root = nil;
- int count, i;
-
- // force flush of changes to objects
- [controller saveToObjects];
- objects = [controller allObjects];
- entity = [realsource entity];
-
- for(count=[objects count],i=0; i < count; i++) {
- id eo = [objects objectAtIndex:i];
- EOQualifier *q = [self makeQualifierForEo:eo entity:entity
- conjoin:isConjoin];
- if (root && q)
- [root disjoinWithQualifier:q];
- else
- root = q;
- }
-
- return root;
- }
-
- - enterQueryMode:sender
- // tweak the controller into QBE state:
- // - no records, buffer edits, save to objects
- {
- DictionaryDataSource *dictsource;
- NSArray *associations;
- int i, count;
- EOEntity *entity;
-
- if (realsource)
- return [self addQuery:nil];
-
- // flush everything we have now
- [controller saveToDataSource];
-
- // swap in tempory datasource
- realsource = [(EODatabaseDataSource *)[controller dataSource] retain];
- dictsource = [[[DictionaryDataSource alloc] init] autorelease];
- [controller setDataSource: dictsource];
-
- // We don't want any detail controllers to be updated to match the
- // QBE entry (since it's meaningless and probably not complete)
-
- // Prepare array to add associations to
- removedAssociations = [[NSMutableArray alloc] init];
-
- // Remove all Qualified Associations
- associations = [controller associations];
- for(count=[associations count], i=0; i < count; i++) {
- EOAssociation *assoc = [associations objectAtIndex: i];
- if (![assoc isQBEEditor]) {
- [removedAssociations addObject: assoc];
- [controller removeAssociation:assoc];
- }
- }
-
- // Remove the next controller (for a fetch)
- nextController = [controller nextController];
- [controller setNextController: nil];
-
- // unset the delegate
- controllersDelegate = [controller delegate];
- [controller setDelegate:nil];
-
- // Should force all "qualifyable" associations (to database attributes)
- // to editable and all others uneditable.
-
- // Make all remaining associations editable
- // Remember those that weren't so we can restore them.
- entity = [realsource entity];
- wereUneditableAssoc = [[NSMutableArray alloc] init];
- wereEditableAssoc = [[NSMutableArray alloc] init];
- associations = [controller associations];
-
- // Note which are uneditable
- for(count=[associations count], i=0; i < count; i++) {
- EOAssociation *assoc = [associations objectAtIndex: i];
- BOOL isQualifyable = ([entity attributeNamed:[assoc key]] != nil);
- if ( isQualifyable && ![assoc isEditable] ) {
- [wereUneditableAssoc addObject:assoc];
- } else if (!isQualifyable && [assoc isEditable]) {
- [wereUneditableAssoc addObject:assoc];
- }
- }
- // make them temporily editable
- for(count=[wereUneditableAssoc count], i=0; i < count; i++) {
- EOAssociation *assoc = [wereUneditableAssoc objectAtIndex: i];
- [assoc setEditable:YES];
- }
- // make them temporily uneditable
- for(count=[wereEditableAssoc count], i=0; i < count; i++) {
- EOAssociation *assoc = [wereEditableAssoc objectAtIndex: i];
- [assoc setEditable:NO];
- }
-
- return [self addQuery:nil];
- }
-
- - addQuery:sender
- {
- int count;
- if (!realsource) return nil;
-
- count = [[controller allObjects] count];
-
- [controller insertObjectAtIndex:count];
- [controller setSelectionIndexes:
- [NSArray arrayWithObject:[NSNumber numberWithInt:count]]];
-
- return self;
- }
-
- - exitQueryMode:sender
- // restore old datasource and controller settings
- {
- int count, i;
-
- // restore datasource
- [controller setDataSource: realsource];
- realsource = nil; // mark that we're out of query mode
-
- // restore removed qualified associations
- for(count=[removedAssociations count], i=0; i<count; i++) {
- EOAssociation *assoc = [removedAssociations objectAtIndex:i];
- [controller addAssociation:assoc];
- }
- [removedAssociations release];
-
- // Reset uneditable associations to back to uneditable again
- for(count=[wereUneditableAssoc count], i=0; i<count; i++) {
- EOAssociation *assoc = [wereUneditableAssoc objectAtIndex:i];
- [assoc setEditable:NO];
- }
- [wereUneditableAssoc release];
-
- // Reset editable associations to back to editable again
- for(count=[wereEditableAssoc count], i=0; i<count; i++) {
- EOAssociation *assoc = [wereEditableAssoc objectAtIndex:i];
- [assoc setEditable:YES];
- }
- [wereEditableAssoc release];
-
-
- // restore next controller
- [controller setNextController:nextController];
-
- // restore the delegate
- [controller setDelegate:controllersDelegate];
-
- [controller fetch];
-
- return self;
- }
-
- - actionOutsideQueryMode
- {
- // We're not in query mode and they pushed a query button.
- // Reset the qualifier and do a basic fetch
- EODatabaseDataSource *source = (EODatabaseDataSource *)[controller dataSource];
- [source setAuxiliaryQualifier:nil];
- [controller fetch];
- return self;
- }
-
- - applyQualifier:sender
- // set qualifier to AND query for current attr settings,
- // exit query mode and fetch
- {
- if (!realsource)
- return [self actionOutsideQueryMode];
-
- [realsource setAuxiliaryQualifier:[self makeQualifier:YES]]; // conjoin
- return [self exitQueryMode:self];
- }
-
- - applyDisjointQualifier:sender
- // set qualifier to AND query for current attr settings,
- // exit query mode and fetch
- {
- if (!realsource)
- return [self actionOutsideQueryMode];
-
- [realsource setAuxiliaryQualifier:[self makeQualifier:NO]]; // Disjoin
- return [self exitQueryMode:self];
- }
-
- - toggleQueryFetch:sender
- {
- return (realsource) ? [self applyQualifier:sender]
- : [self enterQueryMode:sender];
- }
-
- @end
-
-